import * as THREE from 'three';
import Base from './core/Base.js';
import Actor from './core/Actor.js';
import Paths from './Paths.js';
import Color from '../Color.js';

class Vehicles extends Base {
    constructor(root) {
        super();
        this.root = root;
        this.offset = { x: 0, y: 0, z: 0 };
        this.actors = {};
        this.group = null;
        this.currentRoute = new Paths(root);
        this._cached = {}; // The dictionery key is the models file name, cached models quantity and the root mesh. 
        // console.warn('[Vehicles] create');
        this.statuslightActor = null;
        this.statuslightMesh = null;
    }

    destory() {
        // console.error('[Vehicles] destory');
        // (Object.keys(this.actors) || []).forEach(k => this.actors[k].destory());
        // this.actors = {};
        Actor.clearCached();
        this.root && this.root.scene && this.root.scene.remove(this.group);
        this.group = null;
        super.destory();
    }

    // Init status light instance. 
    initStatusLight(res) {
        if (!res || !res.length) return;
        // res.forEach(o => {
        //     let mesh = this.getAlarmMesh(o);
        //     if (this.group && mesh) this.group.add(mesh);
        // });
        let g = new THREE.CircleGeometry(2, 12);
        let geo = new THREE.InstancedBufferGeometry();
        THREE.BufferGeometry.prototype.copy.call(geo, g);
        let mate = new THREE.MeshPhongMaterial({ transparent: true, opacity: 0.5 });
        // let mate = new THREE.MeshStandardMaterial({ transparent: true, opacity: 0.5 });
        let mesh = new THREE.InstancedMesh(geo, mate, res.length);
        mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
        mesh.instanceMatrix.needsUpdate = true;
        res.forEach((o, idx) => mesh.setColorAt(idx, new THREE.Color(0xffffff)));
        mesh.instanceColor.needsUpdate = true;

        mesh.castShadow = true;
        mesh.name = 'statuslight';

        if (this.group && mesh) this.group.add(mesh);
        this.statuslightMesh = mesh;
        // console.warn('Vehicles.initStatusLight: ');
    }
    getAlarmMesh(o) {
        // let shape = new THREE.Shape();
        // shape.moveTo(0, 0);
        // shape.absarc(0, 0, 2, 0, Math.PI * 2, false);
        // let geo = new THREE.ShapeGeometry(shape);
        let geo = new THREE.CircleGeometry(2, 12);
        let mate = new THREE.MeshPhongMaterial({ color: 0x000000 });
        let mesh = new THREE.Mesh(geo, mate);
        let scale = 0.001;
        mesh.position.set((o.tunedPos.x + this.offset.x) / 1000, (o.tunedPos.y + this.offset.y) / 1000, 0);
        // mesh.rotation.y = Math.PI / 2;
        // mesh.scale.set(scale, scale, scale);
        // mesh.rotation.set(rx, ry, rz);
        return mesh;
    }
    updateStatusLight() {}

    setStaticPath(path) {
        this.staticPath = path;
    }

    setOffset({ x = 0, y = 0, z = 0 } = {}) {
        this.offset = { x, y, z };
        this.currentRoute.setOffset({ x, y, z });
    }

    // followVehicleChanged(val) {
    //     this._followVehicle = val;
    // }

    async appendChildren(res) {
        if (!this.group) {
            this.group = new THREE.Group();
            this.group.name = 'vehiclesGroup';
            this.group.rotation.x = Math.PI / -2;
            this.root.scene.add(this.group);
        }

        if (!res || !res.length) return; // TODO: maybe offline, so clear all...
        let castShadow = true;
        if (res.length > 100) castShadow = false; // To ensure rendering performance, when more than 200 vehicles, turn off the shadow. 

        for (let i = 0, len = res.length; i < len; i++) {
            let o = res[i];
            let k = o && o.modelExt && o.modelExt.m3d ? o.modelExt.m3d : '25R.glb';
            let h = (o.modelExt && o.modelExt.m3dOffset && o.modelExt.m3dOffset.h) || 0;
            let act = new Actor(this.root);
            this.actors[o.id] = act;
            let obj = await act.init(o.id, k, castShadow);
            if (obj) {
                let scale = ((o && o.modelExt && o.modelExt.m3dScale) || 1) * 0.001;
                obj.scale.set(scale, scale, scale);
                obj.name = o.id;
                if (h) obj.rotation.y = Math.PI * h;
                obj.userData.vehicleId = o.id;
                obj.traverse(c => (c.isGroup ? (c.userData.vehicleId = o.id) : undefined));
                if (!this.group) break;
                this.group.add(obj);
            }
        }
        this.update(res);
    }
    update(res) {
        if (!res || !res.length || !this.group) return;
        res.forEach((o, idx) => {
            if (!o.tunedPos || !o.tunedPos.x) return;
            let co = this.group.children.find(v => v.name === o.id);
            if (!co) return;
            let ox = (o.modelExt && o.modelExt.m3dOffset && o.modelExt.m3dOffset.x) || 0,
                oy = (o.modelExt && o.modelExt.m3dOffset && o.modelExt.m3dOffset.y) || 0;
            let x = (o.tunedPos.x + this.offset.x + ox) / 1000,
                y = (o.tunedPos.y + this.offset.y + oy) / 1000,
                h = (o.modelExt && o.modelExt.m3dOffset && o.modelExt.m3dOffset.h) || 0;
            co.position.set(x, y, 0);
            co.rotation.y = Math.PI * h + (Math.PI / 180) * o.tunedPos.h;
            this.checkBlinker(o);
            // Display paths, when the vehicle has been selected. 
            if (this.root.settings.selectable && this.root.selectedVehicles.indexOf(o.id) >= 0)
                this.currentRoute.drawPaths(o.routeExt, true);
        });
    }
    clearCache(o) {
        // if (!o) return;
        // o.traverse(function (c) { //eslint-disable-line
        //     if (c.isMesh) {
        //         c.geometry.dispose();
        //         c.material.dispose();
        //     }
        // });
    }

    clearInstances() {
        this._cached = {};
        this.group = null;
    }

    getInstanceKey(o) {
        return `${(o && o.modelExt && o.modelExt.m3d) || ''}`;
    }
    async appendChildrenInstance(res) {
        if (!this.group) {
            this.group = new THREE.Group();
            this.group.name = 'vehiclesGroup';
            this.group.rotation.x = Math.PI / -2;
            this.root.scene.add(this.group);
        }
        // Init status light instance. 
        this.initStatusLight(res);

        if (!res || !res.length) return this.clearInstances(); // TODO: maybe offline, so clear all...

        // Recaculate the vehicles datas, group by m3d keys. 
        res.forEach((o, idx) => {
            // let k = o && o.modelExt && o.modelExt.m3d ? o.modelExt.m3d : '';
            let k = this.getInstanceKey(o);
            if (!this._cached[k]) {
                this._cached[k] = {
                    count: 0,
                    scale: (Number(o && o.modelExt && o.modelExt.m3dScale) || 1000) / 1000,
                    list: []
                };
            }
            this._cached[k].list.push(o);
            this._cached[k].count += 1;
            if (!this._cached[k].statuslightDic) this._cached[k].statuslightDic = {};
            this._cached[k].statuslightDic[o.id] = idx;
        });

        let castShadow = true;
        if (res.length > 100) castShadow = false; // To ensure rendering performance, when more than 200 vehicles, turn off the shadow. 
        if (!this._cached || !Object.keys(this._cached).length) return;

        // Object.keys(this._cached).forEach(k => {
        for (let k in this._cached) {
            let act = new Actor(this.root);
            let rtn = await act.initInstance(k, castShadow, this._cached[k].count);
            if (!rtn || !Array.isArray(rtn) || !rtn.length) return;
            let scale = this._cached[k].scale;
            let g = new THREE.Group();
            g.name = k;
            g.userData.m3d = k;
            rtn.forEach(m => {
                m.scale.set(scale, scale, scale);
                g.add(m);
            });
            if (this.group) this.group.add(g);
            this._cached[k].mesh = g;
            this._cached[k].actor = act;
        }
        this.updateInstance(res);
    }
    getCachedIdx(o) {
        let k = this.getInstanceKey(o);
        let idx = null;
        if (!this._cached || !this._cached[k]) return;
        if (!this._cached[k].dic) this._cached[k].dic = {};
        return this._cached[k].dic[o.id];
    }
    setCachedIdx(idx, o) {
        let k = this.getInstanceKey(o);
        if (!this._cached || !this._cached[k]) return;
        if (!this._cached[k].dic) this._cached[k].dic = {};
        this._cached[k].dic[o.id] = idx;
        return idx;
    }
    updateInstance(res) {
        if (!this._cached) return;
        Object.keys(this._cached).forEach(k => (this._cached[k].rendered = 0));
        if (!res || !res.length) return;
        res.forEach((o, idx) => {
            if (!o.tunedPos || !o.tunedPos.x) return;
            let ox = (o.modelExt && o.modelExt.m3dOffset && o.modelExt.m3dOffset.x) || 0,
                oy = (o.modelExt && o.modelExt.m3dOffset && o.modelExt.m3dOffset.y) || 0,
                x = (o.tunedPos.x + this.offset.x + ox) / 1000,
                y = (o.tunedPos.y + this.offset.y + oy) / 1000,
                h = (o.modelExt && o.modelExt.m3dOffset && o.modelExt.m3dOffset.h) || 0;
            let k = this.getInstanceKey(o);
            if (!this._cached[k]) return;
            let kidx = this.setCachedIdx(this._cached[k] && this._cached[k].rendered, o);
            let mesh = this._cached[k].mesh;
            if (!mesh) return;
            // console.log('------> ', o.id, x, y);
            const dummy = new THREE.Object3D();
            dummy.rotation.x = Math.PI / 2;
            dummy.rotation.y = Math.PI * h + (Math.PI / 180) * o.tunedPos.h;
            dummy.position.set(x, y, 0);
            dummy.updateMatrix();
            mesh.traverse(m => {
                if (!m.isInstancedMesh) return;
                m.setMatrixAt(kidx, dummy.matrix);
                m.instanceMatrix.needsUpdate = true;
                // if (m.name === 'alarm' || m.name === 'warnning' || m.name === 'alarm-light') console.log('---> ', o.id, k, m.name); //eslint-disable-line
            });
            // this._cached[k].dic[o.id] = this._cached[k].rendered++;
            this._cached[k].rendered++;

            // this.statuslightMesh.setMatrixAt(this._cached[k].statuslightDic[o.id], dummy.matrix);

            // this.checkBlinkerInstance(o, this._cached[k].statuslightDic[o.id], dummy.position);
            this.checkBlinkerInstance(o, this._cached[k].statuslightDic[o.id], { x, y });
            // // Display paths, when the vehicle has been selected. 
            // if (this.root.settings.selectable && this.root.selectedVehicles.indexOf(o.id) >= 0)
            //     this.currentRoute.drawPaths(o.routeExt, true);
        });
        this.statuslightMesh.instanceColor.needsUpdate = true;
        this.statuslightMesh.instanceMatrix.needsUpdate = true;
    }

    _getVehicleKey(o) {
        return o && o.modelExt && o.modelExt.m3d ? o.modelExt.m3d : '25R.glb';
    }
    // Normal animations, play the NLA in 3D models. 
    checkBlinker(o) {
        let act = this.actors[o.id];
        if (!act) return;

        // console.log('=========> ', o.modelExt);
        const isDriver = o.modelExt && o.modelExt.type === 'DRIVER',
            hasActionStatus = o.properties && o.properties.actionStatus !== undefined,
            actionStatus = parseInt(o.properties.actionStatus || 0),
            isTrafficSignal = o.modelExt && o.modelExt.m3d && o.modelExt.m3d.indexOf('trafficSignal') >= 0;

        let red = !o.isOnline || !o.isValidated || o.properties.stop === 'true',
            yellow = o.blinkerFillExt && !red; // 车辆action状态

        // console.warn('checkBlinker:', o.id, red, yellow, this.root.settings.showAlarm);
        if (!this.root.settings.showAlarm) act.stop(o.id);
        else if (isDriver && red) act.play(o.id, 'alarm', 'alarm', !!o.blinkerFillExt);
        else if (isDriver && yellow) act.play(o.id, 'warning', 'warning', !!o.blinkerFillExt);
        else if (!isDriver && isTrafficSignal) {
            // let actionName = 'signal0';
            // if (actionStatus === 1) actionName = 'signal1';
            // else if (actionStatus === 2) actionName = 'signal2';
            const actionName = 'signal' + actionStatus;
            if (!hasActionStatus) act.stop(o.id);
            else act.play(o.id, actionName, actionName, !!o.blinkerFillExt);
        } else if (actionStatus >= 0) {
            const actionName = 'alarm' + actionStatus;
            if (!hasActionStatus) act.stop(o.id);
            else act.play(o.id, actionName, actionName, !!o.blinkerFillExt);
        } else act.stop(o.id);
    }
    // Animations in instancedMesh, used setColorAt. 
    checkBlinkerInstance(o, kidx, pos) {
        // if (!this.root.settings.showAlarm) return;
        let key = this.getInstanceKey(o);
        // let act = this.actors[o.id];
        let act = this._cached[key].actor;
        if (!act) return;
        let red = !o.isOnline || !o.isValidated || o.properties.stop === 'true',
            yellow = o.blinkerFillExt && !red;

        let k = this._getVehicleKey(o),
            blinkerFillExt = new THREE.Color(Color.rgbToHex(o.blinkerFillExt));
        if (red) blinkerFillExt = new THREE.Color(Color.rgbToHex('rgba(255, 13, 13, 0.8)'));
        else if (yellow) blinkerFillExt = new THREE.Color(Color.rgbToHex('rgba(255, 255, 0, 0.8)'));

        const dummy = new THREE.Object3D();
        dummy.position.set(pos.x, pos.y, 0);
        if (this.root.settings.showAlarm && (red || yellow)) {
            this.statuslightMesh.setColorAt(kidx, blinkerFillExt);
            dummy.scale.set(1, 1, 1);
        } else {
            dummy.scale.set(0, 0, 0);
        }
        dummy.updateMatrix();
        this.statuslightMesh.setMatrixAt(kidx, dummy.matrix);
    }

    getVehicleIdByInstanceIndex(key, idx) {
        let id = null;
        if (this._cached[key] && this._cached[key].dic)
            id = Object.keys(this._cached[key].dic).find(k => this._cached[key].dic[k] === idx);
        return id;
    }
}

export default Vehicles;
